| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211 |
- 'use client';
- import { useRef, useState } from 'react';
- import { useRouter } from 'next/navigation';
- import { ImageIcon, Camera, Trash2, User } from 'lucide-react';
- import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
- import { fetchApi } from '@/lib/utils/client';
- import { useMemberContext } from '@/contexts/memberProvider';
- import { UserProfileDto } from '@/types/account/profile';
- type Props = {
- open: boolean;
- onOpenChange: (open: boolean) => void;
- profile: UserProfileDto;
- };
- export default function UserProfileEditDialog({ open, onOpenChange, profile }: Props) {
- const router = useRouter();
- const { member, setMember } = useMemberContext();
- const persistMember = (next: typeof member) => {
- if (!next) {
- return;
- }
- setMember(next);
- try {
- localStorage.setItem('member', JSON.stringify(next));
- document.cookie = `member=${encodeURIComponent(JSON.stringify(next))}; path=/; max-age=${60 * 60 * 24 * 30}`;
- } catch {
- // ignore
- }
- };
- const [bannerFile, setBannerFile] = useState<File|null>(null);
- const [bannerPreview, setBannerPreview] = useState<string|null>(profile.bannerUrl);
- const [bannerRemoved, setBannerRemoved] = useState<boolean>(false);
- const [thumbFile, setThumbFile] = useState<File|null>(null);
- const [thumbPreview, setThumbPreview] = useState<string|null>(profile.thumb);
- const [name, setName] = useState<string>(profile.name ?? '');
- const [intro, setIntro] = useState<string>(profile.intro ?? '');
- const [submitting, setSubmitting] = useState(false);
- const [error, setError] = useState<string|null>(null);
- const bannerInputRef = useRef<HTMLInputElement|null>(null);
- const thumbInputRef = useRef<HTMLInputElement|null>(null);
- const handleBannerSelect = (files: FileList|null) => {
- const file = files?.[0];
- if (!file || !file.type.startsWith('image/')) {
- return;
- }
- setBannerFile(file);
- setBannerRemoved(false);
- const reader = new FileReader();
- reader.onloadend = () => setBannerPreview(reader.result as string);
- reader.readAsDataURL(file);
- };
- const handleBannerRemove = () => {
- setBannerFile(null);
- setBannerPreview(null);
- setBannerRemoved(true);
- };
- const handleThumbSelect = (files: FileList|null) => {
- const file = files?.[0];
- if (!file || !file.type.startsWith('image/')) {
- return;
- }
- setThumbFile(file);
- const reader = new FileReader();
- reader.onloadend = () => setThumbPreview(reader.result as string);
- reader.readAsDataURL(file);
- };
- const handleSubmit = async () => {
- if (!member || submitting) {
- return;
- }
- setSubmitting(true);
- setError(null);
- try {
- const nameChanged = name.trim() !== (profile.name ?? '');
- const introChanged = intro !== (profile.intro ?? '');
- if (bannerFile) {
- const form = new FormData();
- form.append('banner', bannerFile);
- const res = await fetchApi<{ bannerUrl: string }>('/api/mypage/banner', { method: 'POST', body: form, silent: true });
- if (!res.success) {
- throw new Error(res.message ?? '배너 업로드 실패');
- }
- } else if (bannerRemoved && profile.bannerUrl) {
- const res = await fetchApi('/api/mypage/banner', { method: 'DELETE', silent: true });
- if (!res.success) {
- throw new Error(res.message ?? '배너 삭제 실패');
- }
- }
- if (thumbFile) {
- const form = new FormData();
- form.append('thumb', thumbFile);
- const res = await fetchApi<{ thumbUrl: string }>('/api/mypage/thumb', { method: 'POST', body: form, silent: true });
- if (!res.success) {
- throw new Error(res.message ?? '프로필 사진 업로드 실패');
- }
- if (member && res.data?.thumbUrl !== undefined) {
- persistMember({ ...member, thumb: res.data.thumbUrl });
- }
- }
- if (nameChanged) {
- const res = await fetchApi('/api/mypage/name', { method: 'POST', body: { Name: name.trim() }, silent: true });
- if (!res.success) {
- throw new Error(res.message ?? '별명 변경 실패');
- }
- if (member) {
- persistMember({ ...member, name: name.trim() });
- }
- }
- if (introChanged) {
- const res = await fetchApi('/api/mypage/intro', { method: 'POST', body: { Intro: intro }, silent: true });
- if (!res.success) {
- throw new Error(res.message ?? '자기소개 변경 실패');
- }
- if (member) {
- persistMember({ ...member, intro });
- }
- }
- onOpenChange(false);
- router.refresh();
- } catch (err) {
- setError(err instanceof Error ? err.message : '저장 실패');
- } finally {
- setSubmitting(false);
- }
- };
- return (
- <Dialog open={open} onOpenChange={onOpenChange}>
- <DialogContent className="user-profile__edit-dialog">
- <DialogHeader>
- <DialogTitle>프로필 편집</DialogTitle>
- </DialogHeader>
- <div className="user-profile__edit-body">
- <div className="user-profile__edit-banner">
- {bannerPreview ? (
- <img src={bannerPreview} alt="배너 미리보기" />
- ) : (
- <div className="user-profile__edit-banner-placeholder" />
- )}
- <div className="user-profile__edit-banner-actions">
- <button type="button" className="user-profile__edit-banner-btn" onClick={() => bannerInputRef.current?.click()} aria-label="배너 변경">
- <ImageIcon size={18} />
- </button>
- {bannerPreview && (
- <button type="button" className="user-profile__edit-banner-btn" onClick={handleBannerRemove} aria-label="배너 제거">
- <Trash2 size={18} />
- </button>
- )}
- </div>
- <input ref={bannerInputRef} type="file" accept="image/*" hidden onChange={(e) => handleBannerSelect(e.target.files)} />
- </div>
- <div className="user-profile__edit-avatar-wrap">
- {thumbPreview ? (
- <img src={thumbPreview} alt="프로필 사진 미리보기" className="user-profile__edit-avatar" />
- ) : (
- <span className="user-profile__edit-avatar user-profile__edit-avatar--empty" aria-hidden="true">
- <User size={40} strokeWidth={1.5} />
- </span>
- )}
- <button type="button" className="user-profile__edit-avatar-btn" onClick={() => thumbInputRef.current?.click()} aria-label="프로필 사진 변경">
- <Camera size={16} />
- </button>
- <input ref={thumbInputRef} type="file" accept="image/*" hidden onChange={(e) => handleThumbSelect(e.target.files)} />
- </div>
- <div className="user-profile__edit-fields">
- <label className="user-profile__edit-field">
- <span>별명</span>
- <input type="text" value={name} onChange={(e) => setName(e.target.value)} maxLength={20} disabled={submitting} />
- </label>
- <label className="user-profile__edit-field">
- <span>자기소개</span>
- <textarea value={intro} onChange={(e) => setIntro(e.target.value)} rows={4} maxLength={500} placeholder="조금 더 자세한 자기소개" disabled={submitting} />
- </label>
- </div>
- {error && <p className="user-profile__edit-error">{error}</p>}
- <div className="user-profile__edit-footer">
- <button type="button" className="user-profile__edit-cancel" onClick={() => onOpenChange(false)} disabled={submitting}>취소</button>
- <button type="button" className="user-profile__edit-save" onClick={handleSubmit} disabled={submitting}>
- {submitting ? '저장 중...' : '저장'}
- </button>
- </div>
- </div>
- </DialogContent>
- </Dialog>
- );
- }
|